/*
 * Copyright 2011 TTOT
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License. You may obtain a copy of
 * the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 */
package ttot.extensionmanager.impl;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Method;
import java.nio.ByteBuffer;
import java.util.LinkedList;
import java.util.List;

import ttot.extension.api.ExtensionModule;
import ttot.extensionmanager.api.ExtensionManager;
import ttot.extensionmanager.api.ExtensionManagerException;
import ttot.extensionmanager.impl.storage.ExtensionStorage;

import com.google.inject.Inject;
import com.google.inject.Injector;

/**
 * Default implementation of the {@link ExtensionManager}.
 * 
 * @author bogdan@krax.ro
 * 
 */
public class ExtensionManagerImpl implements ExtensionManager {

	private ExtensionStorage storage;
	private Injector injector;
	private Injector extensionInjector;

	@Inject
	ExtensionManagerImpl(ExtensionStorage storage, Injector injector) {
		this.storage = storage;
		this.injector = injector;
	}

	/**
	 * Returns the list of extensions loaded by the {@link ExtensionManager}
	 * 
	 * @return {@link List<Extension>}
	 */
	public List<Extension> getExtensions() {
		return storage.get();
	}

	/**
	 * Loads extension classes wrapped in the provided {@link ByteBuffer}. The
	 * ByteBuffer should hold the jar package bytes where the extension classes
	 * are stored. This method converts the buffer in an input stream performs a
	 * call to the load(InputStream) method.
	 */
	public void load(ByteBuffer buffer) throws ExtensionManagerException {
		if (buffer == null || !buffer.hasArray() || buffer.array().length == 0) {
			throw new ExtensionManagerException("Unable to load Extensions from provided ByteBuffer.");
		}
		load(new ByteArrayInputStream(buffer.array()));
	}

	/**
	 * Loads the extension classes read from the provided input stream. The
	 * stream should provided the bytes of the jar package that contains the
	 * extension classes.
	 * 
	 * @throws ExtensionManagerException
	 *             If the inputstream is null, or there are no available bytes
	 *             to read from the stream; wraps an {@link IOException} if
	 *             caught while reading the input stream; wraps an
	 *             {@link IOException} if caught while trying to close the
	 *             provided input stream
	 */
	public void load(InputStream inputStream) throws ExtensionManagerException {
		try {
			if (inputStream == null || inputStream.available() == 0) {
				throw new ExtensionManagerException("Unable to load Extensions from provided InputStream");
			}
		} catch (IOException e) {
			throw new ExtensionManagerException("IOException while verifying the provided InputStream", e);
		} catch (ExtensionManagerException e) {
			throw e;
		} finally {
			try {
				if (inputStream != null) {
					inputStream.close();
				}
			} catch (IOException e) {
				throw new ExtensionManagerException("IOException while trying to close the provided InputStream", e);
			}
		}
		ExtensionClassLoader loader = new ExtensionClassLoader(getClass().getClassLoader());
		try {
			loader.loadJar(inputStream);
			List<Extension> extensions = extensionsIn(loader.getClasses());
			List<ExtensionModule> modules = new LinkedList<ExtensionModule>();
			for (Extension extension : extensions) {
				modules.add(extension.getExtensionModule());
				storage.put(extension);
			}
			extensionInjector = injector.createChildInjector(modules);
			for (ExtensionModule module : modules) {
				extensionInjector.injectMembers(module);
			}
		} catch (IOException e) {
			throw new ExtensionManagerException("IOException while loading InputStream contents", e);
		} catch (ExtensionManagerException e) {
			throw e;
		} finally {
			try {
				inputStream.close();
			} catch (IOException e) {
				throw new ExtensionManagerException("IOException while trying to close the provided InputStream", e);
			}
		}
	}

	/**
	 * Provides a list of Extension objects from the array of classes read from
	 * the input stream.
	 * 
	 * @param classes
	 *            - the classes read from the extension jar package
	 * @return {@link List<Extension>}
	 * @throws ExtensionManagerException
	 *             - if unable to create new instances of ExtensionModule
	 *             classes.
	 * 
	 */
	private static List<Extension> extensionsIn(Class<?>[] classes) throws ExtensionManagerException {
		List<Extension> extensionList = new LinkedList<Extension>();
		for (Class<?> clazz : classes) {
			if (ExtensionModule.class.isAssignableFrom(clazz)) {
				ExtensionModule module;
				try {
					module = (ExtensionModule) clazz.newInstance();
				} catch (Exception e) {
					throw new ExtensionManagerException("Unable to instantiate ExtensionModule class: "
							+ clazz.getCanonicalName(), e);
				}
				extensionList.add(new Extension(module));
			}
		}
		return extensionList;
	}

	/**
	 * Removes all Extensions stored by the ExtensionManager.
	 */
	public void unload() {
		storage.clear();
	}

	/**
	 * Invokes {@link ExtensionModule.afterTest} method on all known Extensions.
	 */
	public void afterTest() throws ExtensionManagerException {
		execute("afterTest");
	}

	/**
	 * Helper method for invoking methods in the known {@link ExtensionModule}
	 * classes.
	 * 
	 * @param methodName
	 * @throws ExtensionManagerException
	 *             - if unable to invoke the specified method; if exceptions are
	 *             caught while executing the specified method
	 */
	private void execute(String methodName) throws ExtensionManagerException {
		List<Exception> exceptions;
		try {
			exceptions = execute(ExtensionModule.class.getMethod(methodName), storage.get());
		} catch (Exception e) {
			throw new ExtensionManagerException(String.format("Exception caught while executing '%s' methods",
					methodName));
		}
		if (exceptions.size() > 0) {
			throw new ExtensionManagerException("Exceptions caught while executing " + methodName + " methods",
					exceptions);
		}
	}

	/**
	 * Helper method that executes the provided method on the provided instance
	 * list.
	 * 
	 * @param method
	 *            - the method to invoke
	 * @param instances
	 *            - the instances that wrap the ExtensionModule instances
	 * @return {@link List<Exception>} - a list of exceptions caught while
	 *         invoking the method on the instances
	 */
	private static List<Exception> execute(Method method, List<Extension> instances) {
		List<Exception> exceptions = new LinkedList<Exception>();
		for (Extension extension : instances) {
			try {
				method.invoke(extension.getExtensionModule());
			} catch (Exception e) {
				exceptions.add(e);
			}
		}
		return exceptions;
	}

	/**
	 * Invokes {@link ExtensionModule.beforeTest} method on all known Extensions.
	 */
	public void beforeTest() throws ExtensionManagerException {
		execute("beforeTest");
	}

	/**
	 * Invokes {@link ExtensionModule.create} method on all known Extensions.
	 */
	public void createExtensions() throws ExtensionManagerException {
		execute("create");
	}

	/**
	 * Invokes {@link ExtensionModule.destroy} method on all known Extensions.
	 */
	public void destroyExtensions() throws ExtensionManagerException {
		execute("destroy");
	}

	public Injector getExtensionInjector() {
		return extensionInjector;
	}

}
